# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt

# pylint: disable=redefined-outer-name

from __future__ import annotations

import sys
import warnings
from contextlib import redirect_stdout
from io import StringIO
from json import dumps
from typing import TYPE_CHECKING

import pytest

from pylint import checkers
from pylint.lint import PyLinter
from pylint.reporters import BaseReporter
from pylint.reporters.text import ParseableTextReporter, TextReporter
from pylint.typing import FileItem

if TYPE_CHECKING:
    from pylint.reporters.ureports.nodes import Section


@pytest.fixture(scope="module")
def reporter():
    return TextReporter


@pytest.fixture(scope="module")
def disable():
    return ["I"]


def test_template_option(linter):
    output = StringIO()
    linter.reporter.out = output
    linter.config.msg_template = "{msg_id}:{line:03d}"
    linter.open()
    linter.set_current_module("0123")
    linter.add_message("C0301", line=1, args=(1, 2))
    linter.add_message("line-too-long", line=2, args=(3, 4))
    assert output.getvalue() == "************* Module 0123\nC0301:001\nC0301:002\n"


def test_template_option_default(linter) -> None:
    """Test the default msg-template setting."""
    output = StringIO()
    linter.reporter.out = output
    linter.open()
    linter.set_current_module("my_module")
    linter.add_message("C0301", line=1, args=(1, 2))
    linter.add_message("line-too-long", line=2, args=(3, 4))

    out_lines = output.getvalue().split("\n")
    assert out_lines[1] == "my_module:1:0: C0301: Line too long (1/2) (line-too-long)"
    assert out_lines[2] == "my_module:2:0: C0301: Line too long (3/4) (line-too-long)"


def test_template_option_end_line(linter) -> None:
    """Test the msg-template option with end_line and end_column."""
    output = StringIO()
    linter.reporter.out = output
    linter.config.msg_template = (
        "{path}:{line}:{column}:{end_line}:{end_column}: {msg_id}: {msg} ({symbol})"
    )
    linter.open()
    linter.set_current_module("my_mod")
    linter.add_message("C0301", line=1, args=(1, 2))
    linter.add_message(
        "line-too-long", line=2, end_lineno=2, end_col_offset=4, args=(3, 4)
    )

    out_lines = output.getvalue().split("\n")
    assert out_lines[1] == "my_mod:1:0::: C0301: Line too long (1/2) (line-too-long)"
    assert out_lines[2] == "my_mod:2:0:2:4: C0301: Line too long (3/4) (line-too-long)"


def test_template_option_non_existing(linter) -> None:
    """Test the msg-template option with non-existent options.
    This makes sure that this option remains backwards compatible as new
    parameters do not break on previous versions
    """
    output = StringIO()
    linter.reporter.out = output
    linter.config.msg_template = (
        "{path}:{line}:{a_new_option}:({a_second_new_option:03d})"
    )
    linter.open()
    with pytest.warns(UserWarning) as records:
        linter.set_current_module("my_mod")
        assert len(records) == 2
        assert (
            "Don't recognize the argument 'a_new_option'" in records[0].message.args[0]
        )
    assert (
        "Don't recognize the argument 'a_second_new_option'"
        in records[1].message.args[0]
    )

    linter.add_message("C0301", line=1, args=(1, 2))
    linter.add_message(
        "line-too-long", line=2, end_lineno=2, end_col_offset=4, args=(3, 4)
    )

    out_lines = output.getvalue().split("\n")
    assert out_lines[1] == "my_mod:1::()"
    assert out_lines[2] == "my_mod:2::()"


def test_deprecation_set_output(recwarn):
    """TODO remove in 3.0."""
    reporter = BaseReporter()
    # noinspection PyDeprecation
    reporter.set_output(sys.stdout)
    warning = recwarn.pop()
    assert "set_output' will be removed in 3.0" in str(warning)
    assert reporter.out == sys.stdout


def test_parseable_output_deprecated():
    with warnings.catch_warnings(record=True) as cm:
        warnings.simplefilter("always")
        ParseableTextReporter()

    assert len(cm) == 1
    assert isinstance(cm[0].message, DeprecationWarning)


def test_parseable_output_regression():
    output = StringIO()
    with warnings.catch_warnings(record=True):
        linter = PyLinter(reporter=ParseableTextReporter())

    checkers.initialize(linter)
    linter.config.persistent = 0
    linter.reporter.out = output
    linter.set_option("output-format", "parseable")
    linter.open()
    linter.set_current_module("0123")
    linter.add_message("line-too-long", line=1, args=(1, 2))
    assert (
        output.getvalue() == "************* Module 0123\n"
        "0123:1: [C0301(line-too-long), ] "
        "Line too long (1/2)\n"
    )


class NopReporter(BaseReporter):
    name = "nop-reporter"
    extension = ""

    def __init__(self, output=None):
        super().__init__(output)
        print("A NopReporter was initialized.", file=self.out)

    def writeln(self, string=""):
        pass

    def _display(self, layout: Section) -> None:
        pass


def test_multi_format_output(tmp_path):
    text = StringIO(newline=None)
    json = tmp_path / "somefile.json"

    source_file = tmp_path / "somemodule.py"
    source_file.write_text('NOT_EMPTY = "This module is not empty"\n')
    escaped_source_file = dumps(str(source_file))

    nop_format = NopReporter.__module__ + "." + NopReporter.__name__
    formats = ",".join(["json:" + str(json), "text", nop_format])

    with redirect_stdout(text):
        linter = PyLinter()
        linter.load_default_plugins()
        linter.set_option("persistent", False)
        linter.set_option("reports", True)
        linter.set_option("score", True)
        linter.set_option("score", True)
        linter.set_option("output-format", formats)

        assert linter.reporter.linter is linter
        with pytest.raises(NotImplementedError):
            linter.reporter.out = text

        linter.open()
        linter.check_single_file_item(FileItem("somemodule", source_file, "somemodule"))
        linter.add_message("line-too-long", line=1, args=(1, 2))
        linter.generate_reports()
        linter.reporter.writeln("direct output")

        # Ensure the output files are flushed and closed
        linter.reporter.close_output_files()
        del linter.reporter

    with open(json, encoding="utf-8") as f:
        assert (
            f.read() == "[\n"
            "    {\n"
            '        "type": "convention",\n'
            '        "module": "somemodule",\n'
            '        "obj": "",\n'
            '        "line": 1,\n'
            '        "column": 0,\n'
            '        "endLine": null,\n'
            '        "endColumn": null,\n'
            f'        "path": {escaped_source_file},\n'
            '        "symbol": "missing-module-docstring",\n'
            '        "message": "Missing module docstring",\n'
            '        "message-id": "C0114"\n'
            "    },\n"
            "    {\n"
            '        "type": "convention",\n'
            '        "module": "somemodule",\n'
            '        "obj": "",\n'
            '        "line": 1,\n'
            '        "column": 0,\n'
            '        "endLine": null,\n'
            '        "endColumn": null,\n'
            f'        "path": {escaped_source_file},\n'
            '        "symbol": "line-too-long",\n'
            '        "message": "Line too long (1/2)",\n'
            '        "message-id": "C0301"\n'
            "    }\n"
            "]\n"
            "direct output\n"
        )

    assert (
        text.getvalue() == "A NopReporter was initialized.\n"
        "************* Module somemodule\n"
        f"{source_file}:1:0: C0114: Missing module docstring (missing-module-docstring)\n"
        f"{source_file}:1:0: C0301: Line too long (1/2) (line-too-long)\n"
        "\n"
        "\n"
        "Report\n"
        "======\n"
        "1 statements analysed.\n"
        "\n"
        "Statistics by type\n"
        "------------------\n"
        "\n"
        "+---------+-------+-----------+-----------+------------+---------+\n"
        "|type     |number |old number |difference |%documented |%badname |\n"
        "+=========+=======+===========+===========+============+=========+\n"
        "|module   |1      |NC         |NC         |0.00        |0.00     |\n"
        "+---------+-------+-----------+-----------+------------+---------+\n"
        "|class    |0      |NC         |NC         |0           |0        |\n"
        "+---------+-------+-----------+-----------+------------+---------+\n"
        "|method   |0      |NC         |NC         |0           |0        |\n"
        "+---------+-------+-----------+-----------+------------+---------+\n"
        "|function |0      |NC         |NC         |0           |0        |\n"
        "+---------+-------+-----------+-----------+------------+---------+\n"
        "\n"
        "\n"
        "\n"
        "3 lines have been analyzed\n"
        "\n"
        "Raw metrics\n"
        "-----------\n"
        "\n"
        "+----------+-------+------+---------+-----------+\n"
        "|type      |number |%     |previous |difference |\n"
        "+==========+=======+======+=========+===========+\n"
        "|code      |2      |66.67 |NC       |NC         |\n"
        "+----------+-------+------+---------+-----------+\n"
        "|docstring |0      |0.00  |NC       |NC         |\n"
        "+----------+-------+------+---------+-----------+\n"
        "|comment   |0      |0.00  |NC       |NC         |\n"
        "+----------+-------+------+---------+-----------+\n"
        "|empty     |1      |33.33 |NC       |NC         |\n"
        "+----------+-------+------+---------+-----------+\n"
        "\n"
        "\n"
        "\n"
        "Duplication\n"
        "-----------\n"
        "\n"
        "+-------------------------+------+---------+-----------+\n"
        "|                         |now   |previous |difference |\n"
        "+=========================+======+=========+===========+\n"
        "|nb duplicated lines      |0     |NC       |NC         |\n"
        "+-------------------------+------+---------+-----------+\n"
        "|percent duplicated lines |0.000 |NC       |NC         |\n"
        "+-------------------------+------+---------+-----------+\n"
        "\n"
        "\n"
        "\n"
        "Messages by category\n"
        "--------------------\n"
        "\n"
        "+-----------+-------+---------+-----------+\n"
        "|type       |number |previous |difference |\n"
        "+===========+=======+=========+===========+\n"
        "|convention |2      |NC       |NC         |\n"
        "+-----------+-------+---------+-----------+\n"
        "|refactor   |0      |NC       |NC         |\n"
        "+-----------+-------+---------+-----------+\n"
        "|warning    |0      |NC       |NC         |\n"
        "+-----------+-------+---------+-----------+\n"
        "|error      |0      |NC       |NC         |\n"
        "+-----------+-------+---------+-----------+\n"
        "\n"
        "\n"
        "\n"
        "Messages\n"
        "--------\n"
        "\n"
        "+-------------------------+------------+\n"
        "|message id               |occurrences |\n"
        "+=========================+============+\n"
        "|missing-module-docstring |1           |\n"
        "+-------------------------+------------+\n"
        "|line-too-long            |1           |\n"
        "+-------------------------+------------+\n"
        "\n"
        "\n"
        "\n"
        "\n"
        "-----------------------------------\n"
        "Your code has been rated at 0.00/10\n"
        "\n"
        "direct output\n"
    )


def test_display_results_is_renamed():
    class CustomReporter(TextReporter):
        def _display(self, layout: Section) -> None:
            return None

    reporter = CustomReporter()
    with pytest.raises(AttributeError) as exc:
        # pylint: disable=no-member
        reporter.display_results()
    assert "no attribute 'display_results'" in str(exc)
